/*
 * Copyright (C) 2010 M. Hautle.
 * 
 * This file is part of SimpleEL.
 * 
 * SimpleEL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * SimpleEL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with SimpleEL. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.simpleel.util;

import java.beans.*;
import java.lang.reflect.*;
import java.util.*;
import java.util.Map.Entry;
import ch.simpleel.*;

/**
 * Helper class for deep bean cloning.<br>
 * Cloning will be done on property level, not on field level!<br>
 * Currently lists, sets, maps, arrays and beans with default constructor were supported.<br>
 * Collection properties must have one of the following types (as declaration): {@link List}, {@link Set} or {@link Map}.<br>
 * You may use a {@link IBeanCloneHook} for controlling the cloning process.
 * 
 * @author M. Hautle
 */
public class BeanCloner {
    // CHECKME auf value accessors umstellen? - property accessor array pro class verwenden nicht einzelne get calls
    /** {@link IBeanCloneHook} doing nothing. */
    protected static final IBeanCloneHook NOP_HOOK = new IBeanCloneHook() {
        @Override
        public void postArrayClone(Object[] src, Object[] clone) {
        }

        @Override
        public void postBeanClone(Object src, Object clone) {
        }

        @Override
        public void postListClone(List src, List clone) {
        }

        @Override
        public void postMapClone(Map src, Map clone) {
        }

        @Override
        public void postSetClone(Set src, Set clone) {
        }

        @Override
        public Class<?> getType(Object src) {
            return src.getClass();
        }

        @Override
        public Object[] preArrayClone(Object[] src) {
            return src;
        }

        @Override
        public Object preBeanClone(Object src) {
            return src;
        }

        @Override
        public List preListClone(List src) {
            return src;
        }

        @Override
        public Map preMapClone(Map src) {
            return src;
        }

        @Override
        public Set preSetClone(Set src) {
            return src;
        }

        @Override
        public Object cloneBean(Object src) {
            return null;
        }

        @Override
        public List cloneList(List src) {
            return null;
        }

        @Override
        public Set cloneSet(Set src) {
            return null;
        }

        @Override
        public Map cloneMap(Map src) {
            return null;
        }

        @Override
        public Object[] cloneArray(Object[] src) {
            return null;
        }
    };

    /**
     * Clones the given bean.
     * 
     * @param <T> The bean type
     * @param bean The bean to clone
     * @return A deep shallow clone of the given bean
     * @throws CloneFailedException If something went wrong
     */
    public static <T> T cloneBean(final T bean) throws CloneFailedException {
        return cloneBean(bean, NOP_HOOK);
    }

    /**
     * Clones a nested bean.
     * 
     * @param <T> Bean Type
     * @param bean The base bean
     * @param path The path to the nested bean
     * @return A clone of the nested bean
     * @throws CloneFailedException If something went wrong
     */
    @SuppressWarnings("unchecked")
    public static <T> T cloneNestedBean(final Object bean, final String path) throws CloneFailedException {
        // necessary cast - otherwise the Sun javac can't compile this class :(
        return (T) cloneNestedBean(bean, path, NOP_HOOK);
    }

    /**
     * Clones the given list.
     * 
     * @param <T> The list content type
     * @param list The source list
     * @return The cloned list
     * @throws CloneFailedException If something went wrong
     */
    public static <T> List<T> cloneList(final List<T> list) throws CloneFailedException {
        return cloneList(list, NOP_HOOK);
    }

    /**
     * Clones the given array.
     * 
     * @param <T> The array content type
     * @param array The source array
     * @return The cloned array
     * @throws CloneFailedException If something went wrong
     */
    public static <T> T[] cloneArray(final T[] array) throws CloneFailedException {
        return cloneArray(array, NOP_HOOK);
    }

    /**
     * Clones the given set.
     * 
     * @param <T> The set content type
     * @param set The source set
     * @return The cloned set
     * @throws CloneFailedException If something went wrong
     */
    public static <T> Set<T> cloneSet(final Set<T> set) throws CloneFailedException {
        return cloneSet(set, NOP_HOOK);
    }

    /**
     * Clones the given map.
     * 
     * @param <K> The key type
     * @param <V> The value type
     * @param map The source map
     * @return The cloned map
     * @throws CloneFailedException If something went wrong
     */
    public static <K, V> Map<K, V> cloneMap(final Map<K, V> map) throws CloneFailedException {
        return cloneMap(map, NOP_HOOK);
    }

    /**
     * Clones the given bean.
     * 
     * @param <T> The bean type
     * @param bean The bean to clone
     * @param hook The post clone hook to use
     * @return A deep shallow clone of the given bean
     * @throws CloneFailedException If something went wrong
     */
    @SuppressWarnings("unchecked")
    public static <T> T cloneBean(final T bean, IBeanCloneHook hook) throws CloneFailedException {
        if (bean == null)
            return null;
        final IdentityHashMap<Object, Object> cache = new IdentityHashMap<Object, Object>();
        final T clone = (T) cloneBean(bean, cache, hook);
        cache.clear();
        return clone;
    }

    /**
     * Clones a nested bean.
     * 
     * @param <T> Bean Type
     * @param bean The base bean
     * @param path The path to the nested bean
     * @param hook The post clone hook to use
     * @return A clone of the nested bean
     * @throws CloneFailedException If something went wrong
     */
    @SuppressWarnings("unchecked")
    public static <T> T cloneNestedBean(final Object bean, final String path, IBeanCloneHook hook) throws CloneFailedException {
        final Object src = ValueAccessorManager.getValueOf(path, bean, null);
        if (src == null)
            return null;
        return cloneBean((T) src, hook);
    }

    /**
     * Clones the given list.
     * 
     * @param <T> The list content type
     * @param list The source list
     * @param hook The post clone hook to use
     * @return The cloned list
     * @throws CloneFailedException If something went wrong
     */
    @SuppressWarnings("unchecked")
    public static <T> List<T> cloneList(final List<T> list, IBeanCloneHook hook) throws CloneFailedException {
        if (list == null)
            return null;
        final IdentityHashMap<Object, Object> cache = new IdentityHashMap<Object, Object>();
        final List<T> clone = (List<T>) cloneList(list, cache, hook);
        cache.clear();
        return clone;
    }

    /**
     * Clones the given set.
     * 
     * @param <T> The set content type
     * @param set The source set
     * @param hook The post clone hook to use
     * @return The cloned set
     * @throws CloneFailedException If something went wrong
     */
    @SuppressWarnings("unchecked")
    public static <T> Set<T> cloneSet(final Set<T> set, IBeanCloneHook hook) throws CloneFailedException {
        if (set == null)
            return null;
        final IdentityHashMap<Object, Object> cache = new IdentityHashMap<Object, Object>();
        final Set<T> clone = (Set<T>) cloneSet(set, cache, hook);
        cache.clear();
        return clone;
    }

    /**
     * Clones the given array.
     * 
     * @param <T> The array content type
     * @param array The source array
     * @param hook The post clone hook to use
     * @return The cloned array
     * @throws CloneFailedException If something went wrong
     */
    @SuppressWarnings("unchecked")
    public static <T> T[] cloneArray(final T[] array, IBeanCloneHook hook) throws CloneFailedException {
        if (array == null)
            return null;
        final IdentityHashMap<Object, Object> cache = new IdentityHashMap<Object, Object>();
        final T[] clone = (T[]) cloneObjectArray(array, cache, hook);
        cache.clear();
        return clone;
    }

    /**
     * Clones the given set.
     * 
     * @param <K> The key type
     * @param <V> The value type
     * @param map The source map
     * @param hook The post clone hook to use
     * @return The cloned map
     * @throws CloneFailedException If something went wrong
     */
    @SuppressWarnings("unchecked")
    public static <K, V> Map<K, V> cloneMap(final Map<K, V> map, IBeanCloneHook hook) throws CloneFailedException {
        if (map == null)
            return null;
        final IdentityHashMap<Object, Object> cache = new IdentityHashMap<Object, Object>();
        final Map<K, V> clone = (Map<K, V>) cloneMap(map, cache, hook);
        cache.clear();
        return clone;
    }

    /**
     * The recursive cloning method.
     * 
     * @param bean The bean to clone
     * @param cache Cache used to detect cycles
     * @param hook The clone hook to use
     * @return A shallow clone of the given bean
     * @throws CloneFailedException If something went wrong
     */
    private static Object cloneBean(Object bean, IdentityHashMap<Object, Object> cache, IBeanCloneHook hook) throws CloneFailedException {
        if (bean == null)
            return null;
        // pre clone hook
        bean = hook.preBeanClone(bean);
        // if the object was already cloned then return this clone
        Object copy = cache.get(bean);
        if (copy != null)
            return copy;
        // clone replacement hook
        copy = hook.cloneBean(bean);
        if (copy != null) {
            cache.put(bean, copy);
            return unwrap(copy);
        }
        try {
            final Class<?> type = hook.getType(bean);
            final PropertyDescriptor[] props = Introspector.getBeanInfo(type).getPropertyDescriptors();
            if (props == null)
                throw new CloneFailedException("Bean cloning failed cause for the bean " + type + " were no PropertyDescriptors defined!");
            int i = 0;
            Method readM = null;
            Method writeM = null;
            // search the first writable property
            for (; i < props.length; i++) {
                if (props[i] instanceof IndexedPropertyDescriptor)
                    continue;
                readM = props[i].getReadMethod();
                writeM = props[i].getWriteMethod();
                if (readM != null && writeM != null)
                    break;
            }
            // return the original bean if no writable property was found
            if (readM == null || writeM == null)
                return bean;
            // clone current bean
            copy = type.newInstance();
            // put into cache (creates same structure and avoids endlessloops....)
            cache.put(bean, copy);
            // clone first property
            writeM.invoke(copy, clonePropertyValue(readM.invoke(bean, (Object[]) null), readM.getReturnType(), cache, hook));
            // clone the other properties
            for (i++; i < props.length; i++) {
                if (props[i] instanceof IndexedPropertyDescriptor)
                    continue;
                readM = props[i].getReadMethod();
                writeM = props[i].getWriteMethod();
                if (readM != null && writeM != null)
                    writeM.invoke(copy, clonePropertyValue(readM.invoke(bean, (Object[]) null), readM.getReturnType(), cache, hook));
            }
            // post clone hook
            hook.postBeanClone(bean, copy);
        } catch (CloneFailedException e) {
            throw e;
        } catch (Exception e) {
            throw new CloneFailedException("Bean cloning failed!", e);
        }
        return copy;
    }

    /**
     * Clones a property value.<br>
     * This is 'just' a dispatcher method for bean properties.<br>
     * The difference to {@link #cloneValue(Object, IdentityHashMap, IBeanCloneHook)} is that we have to consider the property type; a {@link LinkedList} is
     * also a {@link List} but if the property specifies a {@link LinkedList} we can't return an {@link ArrayList}!
     * 
     * @param value The value of the property (may be null)
     * @param type The type of the property
     * @param cache Cache used to detect cycles
     * @param hook The post clone hook to use
     * @return The cloned value
     * @throws CloneFailedException If something went wrong
     */
    private static Object clonePropertyValue(final Object value, Class type, final IdentityHashMap<Object, Object> cache, IBeanCloneHook hook)
            throws CloneFailedException {
        if (value == null)
            return null;
        if (type.isArray())
            return cloneArray(value, cache, hook);
        if (type.equals(List.class))
            return cloneList((List) value, cache, hook);
        if (type.equals(Set.class))
            return cloneSet((Set) value, cache, hook);
        if (type.equals(Map.class))
            return cloneMap((Map) value, cache, hook);
        return cloneBean(value, cache, hook);
    }

    /**
     * Clones a value.<br>
     * This is 'just' a dispatcher method for non bean content (lists, sets etc.).<br>
     * 
     * @param value The value or null
     * @param cache Cache used to detect cycles
     * @param hook The post clone hook to use
     * @return The cloned value
     * @throws CloneFailedException If something went wrong
     */
    private static Object cloneValue(final Object value, final IdentityHashMap<Object, Object> cache, IBeanCloneHook hook) throws CloneFailedException {
        if (value == null)
            return null;
        if (value instanceof Object[])
            return cloneObjectArray((Object[]) value, cache, hook);
        if (value instanceof List)
            return cloneList((List) value, cache, hook);
        if (value instanceof Set)
            return cloneSet((Set) value, cache, hook);
        if (value instanceof Map)
            return cloneMap((Map) value, cache, hook);
        // check if the value has a component type - then it must be a primitive value array
        if (value.getClass().getComponentType() != null)
            return clonePrimitiveArray(value, cache);
        return cloneBean(value, cache, hook);
    }

    /**
     * Clones the given list.
     * 
     * @param list The source list
     * @param cache Cache used to detect cycles
     * @param hook The post clone hook to use
     * @return The cloned list
     * @throws CloneFailedException If something went wrong
     */
    @SuppressWarnings("unchecked")
    private static Object cloneList(List list, final IdentityHashMap<Object, Object> cache, IBeanCloneHook hook) throws CloneFailedException {
        // pre clone hook
        list = hook.preListClone(list);
        Object o = cache.get(list);
        if (o != null)
            return o;
        // clone replacement hook
        o = hook.cloneList(list);
        if (o != null) {
            cache.put(list, o);
            return unwrap(o);
        }
        final List clone = new ArrayList(list.size());
        cache.put(list, clone);
        for (int i = 0, cnt = list.size(); i < cnt; i++)
            clone.add(cloneValue(list.get(i), cache, hook));
        // post clone hook
        hook.postListClone(list, clone);
        return clone;
    }

    /**
     * Clones the given array.
     * 
     * @param array The source array
     * @param cache Cache used to detect cycles
     * @param hook The post clone hook to use
     * @return The cloned array
     * @throws CloneFailedException If something went wrong
     */
    private static Object cloneArray(Object array, final IdentityHashMap<Object, Object> cache, IBeanCloneHook hook) throws CloneFailedException {
        if (array instanceof Object[])
            return cloneObjectArray((Object[]) array, cache, hook);
        return clonePrimitiveArray(array, cache);
    }

    /**
     * Clones the given primitive array.
     * 
     * @param array The primitive array
     * @param cache Cache used to detect cycles
     * @return The cloned array
     */
    private static Object clonePrimitiveArray(Object array, IdentityHashMap<Object, Object> cache) {
        final Object o = cache.get(array);
        if (o != null)
            return o;
        final int len = Array.getLength(array);
        final Object clone = Array.newInstance(array.getClass().getComponentType(), len);
        System.arraycopy(array, 0, clone, 0, len);
        cache.put(array, clone);
        return clone;
    }

    /**
     * Clones the given array.
     * 
     * @param array The source array
     * @param cache Cache used to detect cycles
     * @param hook The post clone hook to use
     * @return The cloned array
     * @throws CloneFailedException If something went wrong
     */
    private static Object cloneObjectArray(Object[] array, final IdentityHashMap<Object, Object> cache, IBeanCloneHook hook) throws CloneFailedException {
        // pre clone hook
        array = hook.preArrayClone(array);
        Object o = cache.get(array);
        if (o != null)
            return o;
        // clone replacement hook
        o = hook.cloneArray(array);
        if (o != null) {
            cache.put(array, o);
            return unwrap(o);
        }
        final Object[] clone = (Object[]) Array.newInstance(array.getClass().getComponentType(), array.length);
        cache.put(array, clone);
        for (int i = 0; i < clone.length; i++)
            clone[i] = cloneValue(array[i], cache, hook);
        // post clone hook
        hook.postArrayClone(array, clone);
        return clone;
    }

    /**
     * Clones the given map.
     * 
     * @param map The source map
     * @param cache Cache used to detect cycles
     * @param hook The post clone hook to use
     * @return The cloned map
     * @throws CloneFailedException If something went wrong
     */
    @SuppressWarnings("unchecked")
    private static Object cloneMap(Map<?, ?> map, final IdentityHashMap<Object, Object> cache, IBeanCloneHook hook) throws CloneFailedException {
        // pre clone hook
        map = hook.preMapClone(map);
        Object o = cache.get(map);
        if (o != null)
            return map;
        // clone replacement hook
        o = hook.cloneMap(map);
        if (o != null) {
            cache.put(map, o);
            return unwrap(o);
        }
        final Map clone = new HashMap(map.size());
        cache.put(map, clone);
        for (Entry entry : map.entrySet())
            clone.put(entry.getKey(), cloneValue(entry.getValue(), cache, hook));
        // post clone hook
        hook.postMapClone(map, clone);
        return clone;
    }

    /**
     * Clones the given set.
     * 
     * @param set The source set
     * @param cache Cache used to detect cycles
     * @param hook The post clone hook to use
     * @return The cloned set
     * @throws CloneFailedException If something went wrong
     */
    @SuppressWarnings("unchecked")
    private static Object cloneSet(Set set, final IdentityHashMap<Object, Object> cache, IBeanCloneHook hook) throws CloneFailedException {
        // pre clone hook
        set = hook.preSetClone(set);
        Object o = cache.get(set);
        if (o != null)
            return o;
        // clone replacement hook
        o = hook.cloneSet(set);
        if (o != null) {
            cache.put(set, o);
            return unwrap(o);
        }
        final Set clone = new HashSet(set.size());
        cache.put(set, clone);
        for (Object e : set)
            clone.add(cloneValue(e, cache, hook));
        // post clone hook
        hook.postSetClone(set, clone);
        return clone;
    }

    /**
     * Simply replaces {@link IBeanCloneHook#NULL_VALUE} through <code>null</code>.
     * 
     * @param value The value or null
     * @return The unwrapped value
     */
    protected static Object unwrap(Object value) {
        return value != IBeanCloneHook.NULL_VALUE ? value : null;
    }
}
